sql.js ➔ ... ➔ ???   A
last analyzed

Complexity

Conditions 2
Paths 3

Size

Total Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
c 0
b 0
f 0
nc 3
nop 1
dl 0
loc 8
rs 9.4285
1
import {parse} from 'node-sqlparser'
2
import {Promise} from 'bluebird'
3
import path from 'path'
4
import {
5
  coreUtils,
6
  config,
7
  Manager,
8
  cmsData
9
} from '../../'
10
11
/**
12
 * take a string and json to escape sql character and convert to sql like syntax
13
 *
14
 * Example: escapeAbeValuesFromStringRequest('select title from ./ where `abe_meta.template`=`{{article}}`', {article: "test"})
15
 *
16
 * Return string: select title from ___abe_dot______abe_dot______abe___ where  `abe_meta.template`=`test`
17
 * 
18
 * 
19
 * @param  {String} str      raw abe request sql string
20
 * @param  {Object} jsonPage json object of post
21
 * @return {String}          escaped string
22
 */
23
export function escapeAbeValuesFromStringRequest(str, jsonPage) {
0 ignored issues
show
Unused Code introduced by
The parameter jsonPage is not used and could be removed.

This check looks for parameters in functions that are not used in the function body and are not followed by other parameters which are used inside the function.

Loading history...
24
  var matchFrom = /from .(.*?) /
25
  var matchVariable = /{{([a-zA-Z]*)}}/
26
27
  var matchFromExec = matchFrom.exec(str)
28
  if(matchFromExec != null && matchFromExec[1] != null) {
29
30
    var fromMatch
31
    var toReplace = matchFromExec[1]
32
    while (fromMatch = matchVariable.exec(toReplace)) {
33
      try {
34
        var value = eval('jsonPage.' + fromMatch[1])
0 ignored issues
show
Security Performance introduced by
Calls to eval are slow and potentially dangerous, especially on untrusted code. Please consider whether there is another way to achieve your goal.
Loading history...
35
        if(value != null) {
36
          toReplace = toReplace.replace('{{' + fromMatch[1] + '}}', value)
37
        }else {
38
          toReplace = toReplace.replace('{{' + fromMatch[1] + '}}', '')
39
        }
40
      }catch(e) {
0 ignored issues
show
Coding Style Comprehensibility Best Practice introduced by
Empty catch clauses should be used with caution; consider adding a comment why this is needed.
Loading history...
41
      }
42
    }
43
44
    str = str.replace(matchFromExec[1], toReplace)
45
  }
46
47
  var from = /from ([\S\s]+)/.exec(str)
48
49
  var matches = from
50
  if(matches[1]) {
51
    var res = matches[1]
52
    var splitAttr = [' where ', ' order by ', ' limit ', ' WHERE ', ' ORDER BY ', ' LIMIT ']
53
    for(var i = 0; i < splitAttr.length; i++) {
54
      if(res.indexOf(splitAttr[i]) > -1) {
55
        res = res.substring(0, res.indexOf(splitAttr[i]))
56
      }
57
    }
58
    var escapedFrom = res.replace(/\//g, '___abe___')
59
    escapedFrom = escapedFrom.replace(/\./g, '___abe_dot___')
60
    escapedFrom = escapedFrom.replace(/-/g, '___abe_dash___')
61
    str = str.replace(res, escapedFrom)
62
  }
63
64
  str = str.replace(/``/g, '\'\'')
65
66
  return str
67
}
68
69
/**
70
 * analyse and create an object from request sql string
71
 *
72
 * Example: handleSqlRequest('select title from ./ where `abe_meta.template`=`{{article}}`', {article: "test"})
73
 * 
74
 * @param  {String} str      Sql string request
75
 * @param  {Object} jsonPage json of post
76
 * @return {Object}          {type, columns, from, where, string, limit, orderby}
77
 */
78
export function handleSqlRequest(str, jsonPage) {
79
  var req = escapeAbeValuesFromStringRequest(str, jsonPage)
80
  var request = parse(req)
81
  var reconstructSql = ''
82
83
  // SQL TYPE
84
  var type = ''
85
  if(request.type != null) {
86
    type = request.type
87
  }
88
  reconstructSql += `${type} `
89
90
  // SQL COLUMNS
91
  var columns = []
92
  if(request.columns != null) {
93
    if(request.columns === '*') {
94
      columns.push('*')
95
    }else {
96
      Array.prototype.forEach.call(request.columns, (item) => {
97
        columns.push(item.expr.column)
98
      })
99
    }
100
  }
101
  reconstructSql += `${JSON.stringify(columns)} `
102
103
  // SQL FROM
104
  var from = []
105
  if(request.from != null) {
106
107
    Array.prototype.forEach.call(request.from, (item) => {
108
      from.push(item.table)
109
    })
110
  }else {
111
    from.push('*')
112
  }
113
  reconstructSql += `from ${JSON.stringify(from)} `
114
115
  var where = null
116
  if(request.where != null) {
117
    where = request.where
118
  }
119
120
  var limit = -1
121
  if(request.limit != null) {
122
    limit = request.limit[request.limit.length - 1].value
123
  }
124
125
  var orderby = null
126
  if(request.orderby != null && request.orderby.length > 0) {
127
    orderby = {
128
      column: request.orderby[0].expr.column,
129
      type: request.orderby[0].type
130
    }
131
    reconstructSql += `ORDER BY ${orderby.column} ${orderby.type} `
132
  }
133
134
  return {
135
    type: type,
136
    columns: columns,
137
    from: from,
138
    where: where,
139
    string: reconstructSql,
140
    limit: limit,
141
    orderby: orderby
142
  }
143
}
144
145
/**
146
 * get JSON from abe tag attribute source
147
 *
148
 * {{abe type='data' key='titles' desc='select titles' source='[{"title": "rouge", "id": 1},{"title": "vert", "id": 2},{"title": "blue", "id": 3}]' display="{{title}}"}}
149
 *
150
 * return 
151
 * [{"title": "rouge", "id": 1},{"title": "vert", "id": 2},{"title": "blue", "id": 3}]
152
 * 
153
 * @param  {String} str abe tag
154
 * @return {String}     json string
155
 */
156
export function getDataSource(str) {
157
  var reg = /source=(['|"])(.*?)\1[ |\}]/g
158
  var match = reg.exec(str)
159
  if (match != null) {
160
    return match[2]
161
  }
162
163
  return ''
164
}
165
166
/**
167
 * replaces escaped characters with the right ones
168
 * @param  {String} statement the from clause
169
 * @return {String}           the from sanitized
170
 */
171
export function sanitizeFromStatement(statement){
172
  var from = ''
173
174
  if(statement != null) {
175
    from = statement[0].replace(/___abe_dot___/g, '.')
176
    from = from.replace(/___abe___/g, '/')
177
    from = from.replace(/___abe_dash___/g, '-')
178
  }
179
180
  return from
181
}
182
183
/**
184
 * calculate the directory to analyze from the from clause
185
 * @param  {String} statement the from clause
186
 * @param  {String} tplPath   the path from the template originator
187
 * @return {string}           the directory to analyze
188
 */
189
export function getFromDirectory(statement, tplPath){
190
  var pathFromDir = ''
191
  if(!tplPath){
192
    tplPath = '/'
193
  }
194
195
  if(statement === '' || statement === '*' || statement === '/') {
196
    pathFromDir = path.join(config.root, config.data.url)
197
  }else if(statement === './') {
198
    pathFromDir = path.join(config.root, config.data.url, tplPath)
199
  }else if(statement.indexOf('/') === 0) {
200
    pathFromDir = path.join(config.root, config.data.url, statement)
201
  }else if(statement.indexOf('/') !== 0) {
202
    pathFromDir = path.join(config.root, config.data.url, tplPath, statement)
203
  }
204
205
  return pathFromDir
206
}
207
208
/**
209
 * sort array of files from where clause
210
 *
211
 * @param  {Array} files
212
 * @param  {Object} orderby {orderby: {column: 'date'}} | {orderby: {column: 'random', type: 'ASC'}}
213
 * @return {Array}         sorted array
214
 */
215
export function executeOrderByClause(files, orderby){
216
  if(orderby != null) {
217
    if(orderby.column.toLowerCase() === 'random') {
218
      files = coreUtils.sort.shuffle(files)
219
    }else if(orderby.column.toLowerCase() === 'date') {
220
      if(orderby.type === 'ASC') {
221
        files.sort(cmsData.sort.byDateAsc)
222
      }else if(orderby.type === 'DESC') {
223
        files.sort(cmsData.sort.byDateDesc)
224
      }
225
    }
226
  }
227
228
  return files
229
}
230
231
/**
232
 * Keep only published post
233
 *
234
 * keepOnlyPublishedPost([files])
235
 *
236
 * @param  {Array} files      paths
237
 * @return {Array}                files
238
 */
239
export function keepOnlyPublishedPost(files){
240
  var publishedValue = []
241
  Array.prototype.forEach.call(files, (file) => {
242
    if (file.publish != null) {
243
      publishedValue.push(file.publish)
244
    }
245
  })
246
247
  return publishedValue
248
}
249
250
/**
251
 * Check array of files have path that match path statement
252
 *
253
 * executeFromClause([array], ['/'], ['/'])
254
 *
255
 * @param  {Array} statement      paths
256
 * @param  {Array} pathFromClause paths
257
 * @return {Array}                files
258
 */
259
export function executeFromClause(files, statement, pathFromClause){
260
  var from = sanitizeFromStatement(statement)
261
262
  // if the from clause ends with a dot, we won't recurse the directory analyze
263
  if(from.slice(-1) === '.'){
264
    from = from.slice(0, -1)
265
  }
266
  
267
  var fromDirectory = getFromDirectory(from, pathFromClause)
268
269
  var files_array = files.filter((element) => {
270
    if (element.path.indexOf(fromDirectory) > -1) {
271
      return true
272
    }
273
    return false
274
  })
275
276
  return files_array
277
}
278
279
/**
280
 * Execute sql query like to find abe json post that match the query
281
 * 
282
 * @param  {Array} pathQuery of paths
283
 * @param  {String} match     request sql
284
 * @param  {Object} jsonPage  json of post
285
 * @return {Array}           found object that match
286
 */
287
export function execQuery(pathQuery, match, jsonPage) {
288
  var files = keepOnlyPublishedPost(Manager.instance.getList())
289
  var request = handleSqlRequest(cmsData.regex.getAttr(match, 'source'), jsonPage)
290
291
  files = executeFromClause(files, request.from, pathQuery)
292
  files = executeWhereClause(files, request.where, request.limit, request.columns, jsonPage)
293
  files = executeOrderByClause(files, request.orderby)
294
  return files
295
}
296
297
export function executeQuerySync(pathQuerySync, match, jsonPage) {
298
  return execQuery(pathQuerySync, match, jsonPage)
299
}
300
301
export function executeQuery(pathexecuteQuery, match, jsonPage) {
302
  var p = new Promise((resolve) => {
303
    var res = execQuery(pathexecuteQuery, match, jsonPage)
304
    resolve(res)
305
  }).catch(function(e) {
306
    console.error(e)
307
  })
308
309
  return p
310
}
311
312
/**
313
 * check if a given string an url, string json, file url, abe sql request
314
 * 
315
 * get('http://google.com')
316
 * get('{"test":"test"}')
317
 * get('select * from ../')
318
 * get('test')
319
 * 
320
 * @param  {String} str 
321
 * @return {String} url | request | value | file | other
322
 */
323
export function getSourceType(str) {
324
  if(/http:\/\/|https:\/\//.test(str)) {
325
    return 'url'
326
  }
327
328
  if(/select[\S\s]*?from/.test(str)) {
329
    return 'request'
330
  }
331
332
  try {
333
    JSON.parse(str)
334
    return 'value'
335
  }catch(e) {
0 ignored issues
show
Coding Style Comprehensibility Best Practice introduced by
Empty catch clauses should be used with caution; consider adding a comment why this is needed.
Loading history...
336
337
  }
338
339
  if(/\.json/.test(str)) {
340
    return 'file'
341
  }
342
343
  return 'other'
344
}
345
346
/**
347
 * return array of post that match sql where statement
348
 *
349
 * Example: handleSqlRequest('select title from ./ where `abe_meta.template`=`article`', {})
350
 *
351
 * @param  {Array} files    
352
 * @param  {Object} wheres   clause
353
 * @param  {Int} maxLimit 
354
 * @param  {Array} columns  sql
355
 * @param  {Object} jsonPage json post
356
 * @return {Array}          of files
357
 */
358
export function executeWhereClause(files, wheres, maxLimit, columns, jsonPage){
359
  if(typeof wheres === 'undefined' || wheres === null) return files
0 ignored issues
show
Coding Style Best Practice introduced by
Curly braces around statements make for more readable code and help prevent bugs when you add further statements.

Consider adding curly braces around all statements when they are executed conditionally. This is optional if there is only one statement, but leaving them out can lead to unexpected behaviour if another statement is added later.

Consider:

if (a > 0)
    b = 42;

If you or someone else later decides to put another statement in, only the first statement will be executed.

if (a > 0)
    console.log("a > 0");
    b = 42;

In this case the statement b = 42 will always be executed, while the logging statement will be executed conditionally.

if (a > 0) {
    console.log("a > 0");
    b = 42;
}

ensures that the proper code will be executed conditionally no matter how many statements are added or removed.

Loading history...
360
  var res = []
361
  var limit = 0
362
  var json = {}
363
  var jsonValues = {}
364
365
  for(let file of files) {
366
    if(limit < maxLimit || maxLimit === -1) {
367
      if(wheres != null) {
368
        if(!recurseWhere(wheres, file, jsonPage)) {
369
          json = JSON.parse(JSON.stringify(file))
370
          jsonValues = {}
371
372
          if(columns != null && columns.length > 0 && columns[0] !== '*') {
373
            Array.prototype.forEach.call(columns, (column) => {
374
              if(json[column] != null) {
0 ignored issues
show
Bug introduced by
The variable json is changed as part of the for-each loop for example by JSON.parse(JSON.stringify(file)) on line 369. Only the value of the last iteration will be visible in this function if it is called after the loop.
Loading history...
375
                jsonValues[column] = json[column]
0 ignored issues
show
Bug introduced by
The variable jsonValues is changed as part of the for-each loop for example by {} on line 370. Only the value of the last iteration will be visible in this function if it is called after the loop.
Loading history...
376
              }
377
            })
378
            jsonValues['abe_meta'] = json['abe_meta']
379
          }else {
380
            jsonValues = json
381
          }
382
383
          res.push(jsonValues)
384
          limit++
385
        }
386
      }
387
    } else {
388
      break
389
    }
390
  }
391
392
  return res
393
}
394
395
/**
396
 * Compare where left and where right clause
397
 * 
398
 * @param  {Object} where           clause
399
 * @param  {Object} jsonDoc         json of current post
400
 * @param  {Object} jsonOriginalDoc json of post to compare
401
 * @return {Object}                 {left: value, right: value}
402
 */
403
export function getWhereValuesToCompare(where, jsonDoc, jsonOriginalDoc) {
0 ignored issues
show
Unused Code introduced by
The parameter jsonDoc is not used and could be removed.

This check looks for parameters in functions that are not used in the function body and are not followed by other parameters which are used inside the function.

Loading history...
Unused Code introduced by
The parameter jsonOriginalDoc is not used and could be removed.

This check looks for parameters in functions that are not used in the function body and are not followed by other parameters which are used inside the function.

Loading history...
404
  var regexIsVariable = /^{{(.*)}}$/
405
  var value = null
406
  var compare = null
407
408
  try {
409
    var variableLeft = where.left.column
410
    var whereLeftColumn = where.left.column
411
    var checkIfLeftIsAVariable = regexIsVariable.exec(variableLeft)
412
    if(checkIfLeftIsAVariable != null && checkIfLeftIsAVariable.length > 0) {
413
      variableLeft = checkIfLeftIsAVariable[1]
414
    }
415
    value = eval('jsonDoc.' + variableLeft)
0 ignored issues
show
Security Performance introduced by
Calls to eval are slow and potentially dangerous, especially on untrusted code. Please consider whether there is another way to achieve your goal.
Loading history...
416
  }catch(e) {
417
    // console.log('e', e)
418
  }
419
  
420
  if(where.operator === 'IN' || where.operator === 'NOT IN') {
421
    if(value == null) {
422
      // is the form of variableLeft something like elt.attribute or elt[].attribute?
423
      // so maybe is elt an array ?
424
      let arValues = variableLeft.split('.')
425
      let key = arValues[0]
426
      if(key.slice(-2) == '[]'){
427
        key = key.slice(0, -2)
428
      }
429
      let records = eval('jsonDoc.' + key)
0 ignored issues
show
Security Performance introduced by
Calls to eval are slow and potentially dangerous, especially on untrusted code. Please consider whether there is another way to achieve your goal.
Loading history...
430
      whereLeftColumn = arValues[1]
431
      value = []
432
      // if yes, value is then an array of values
433
      if(records != null) {
434
        Array.prototype.forEach.call(records, (record) => {
435
          try {
436
            let val = record[arValues[1]]
437
            value.push(val)
438
          } catch (e) {
439
            console.log(e.stack)
0 ignored issues
show
Debugging Code introduced by
console.log looks like debug code. Are you sure you do not want to remove it?
Loading history...
440
          }
441
        })
442
      }
443
    }
444
    compare = []
445
    Array.prototype.forEach.call(where.right.value, (right) => {
446
      var matchRightVariable = regexIsVariable.exec(right.column)
447
      if(matchRightVariable != null && matchRightVariable.length > 0) {
448
        try {
449
          var jsonOriginalValues = eval('jsonOriginalDoc.' + matchRightVariable[1])
0 ignored issues
show
Security Performance introduced by
Calls to eval are slow and potentially dangerous, especially on untrusted code. Please consider whether there is another way to achieve your goal.
Loading history...
450
          Array.prototype.forEach.call(jsonOriginalValues, (jsonOriginalValue) => {
0 ignored issues
show
Unused Code introduced by
The parameter jsonOriginalValue is not used and could be removed.

This check looks for parameters in functions that are not used in the function body and are not followed by other parameters which are used inside the function.

Loading history...
451
            compare.push(eval('jsonOriginalValue.' + whereLeftColumn))
0 ignored issues
show
Bug introduced by
The variable whereLeftColumn does not seem to be initialized in case var variableLeft = where.left.column on line 409 throws an error. Are you sure this can never be the case?
Loading history...
Security Performance introduced by
Calls to eval are slow and potentially dangerous, especially on untrusted code. Please consider whether there is another way to achieve your goal.
Loading history...
452
          })
453
        }catch(e) {}
0 ignored issues
show
Coding Style Comprehensibility Best Practice introduced by
Empty catch clauses should be used with caution; consider adding a comment why this is needed.
Loading history...
454
      }
455
      else{
456
        compare.push(right.column)
457
      }
458
    })
459
  } else {
460
    if(where.right.column != null) {
461
      compare = where.right.column
462
    } else if(where.right.value != null) {
463
      compare = where.right.value
464
    }
465
466
    var matchRightVariable = regexIsVariable.exec(compare)
467
468
    if(matchRightVariable != null && matchRightVariable.length > 0) {
469
      try {
470
        var shouldCompare = eval('jsonOriginalDoc.' + matchRightVariable[1])
0 ignored issues
show
Security Performance introduced by
Calls to eval are slow and potentially dangerous, especially on untrusted code. Please consider whether there is another way to achieve your goal.
Loading history...
471
        if(shouldCompare != null) {
472
          compare = shouldCompare
473
        }else {
474
          compare = null
475
        }
476
      }catch(e) {
477
        compare = null
478
      }
479
    }
480
  }
481
482
  return {
483
    left: value,
484
    right: compare
485
  }
486
}
487
488
export function isInStatementCorrect(values, isCorrect){
489
  if( Object.prototype.toString.call(values.left) === '[object Array]' ) {
490
    Array.prototype.forEach.call(values.left, (left) => {
491
      Array.prototype.forEach.call(values.right, (right) => {
492
        if(left != null && left === right) {
493
          isCorrect = !isCorrect
494
        }
495
      })
496
    })
497
  } else {
498
    Array.prototype.forEach.call(values.right, (right) => {
499
      if(values.left === right) {
500
        isCorrect = !isCorrect
501
      }
502
    })
503
  }
504
505
  return isCorrect
506
}
507
508
/**
509
 * Check where.left value that match where operator (=, !=, >, >=, <, <=, LIKE, NOT LIKE, AND, OR, IN, NOT IN)
510
 * if operator AND or OR
511
 * Recurse on where.left and where.right sql clause
512
 *
513
 * 
514
 * @param  {Object} where           clause
515
 * @param  {Object} jsonDoc         json of current post
516
 * @param  {Object} jsonOriginalDoc json of post to compare
517
 * @return {Boolean}                 true if not matching | false if matching
518
 */
519
export function recurseWhere(where, jsonDoc, jsonOriginalDoc) {
520
  var isNotLeftCorrect = false
0 ignored issues
show
Unused Code introduced by
The assignment to variable isNotLeftCorrect seems to be never used. Consider removing it.
Loading history...
521
  var isNotRightCorrect = false
0 ignored issues
show
Unused Code introduced by
The assignment to variable isNotRightCorrect seems to be never used. Consider removing it.
Loading history...
522
  var isNotCorrect = false
523
  var values
524
525
  switch(where.operator) {
0 ignored issues
show
Coding Style introduced by
As per coding-style, switch statements should have a default case.
Loading history...
526
  case '=':
527
    values = getWhereValuesToCompare(where, jsonDoc, jsonOriginalDoc)
528
    isNotCorrect = !(values.left === values.right)
529
    break
530
  case '!=':
531
    values = getWhereValuesToCompare(where, jsonDoc, jsonOriginalDoc)
532
    isNotCorrect = !(values.left !== values.right)
533
    break
534
  case '>':
535
    values = getWhereValuesToCompare(where, jsonDoc, jsonOriginalDoc)
536
    isNotCorrect = !(values.left > values.right)
537
    break
538
  case '>=':
539
    values = getWhereValuesToCompare(where, jsonDoc, jsonOriginalDoc)
540
    isNotCorrect = !(values.left >= values.right)
541
    break
542
  case '<':
543
    values = getWhereValuesToCompare(where, jsonDoc, jsonOriginalDoc)
544
    isNotCorrect = !(values.left < values.right)
545
    break
546
  case '<=':
547
    values = getWhereValuesToCompare(where, jsonDoc, jsonOriginalDoc)
548
    isNotCorrect = !(values.left <= values.right)
549
    break
550
  case 'LIKE':
551
    values = getWhereValuesToCompare(where, jsonDoc, jsonOriginalDoc)
552
    isNotCorrect = !(values.left && values.left.indexOf(values.right) > -1)
553
    break
554
  case 'NOT LIKE':
555
    values = getWhereValuesToCompare(where, jsonDoc, jsonOriginalDoc)
556
    isNotCorrect = !(values.left && values.left.indexOf(values.right) === -1)
557
    break
558
  case 'AND':
559
    isNotLeftCorrect = recurseWhere(where.left, jsonDoc, jsonOriginalDoc)
560
    isNotRightCorrect = recurseWhere(where.right, jsonDoc, jsonOriginalDoc)
561
    isNotCorrect = (isNotLeftCorrect || isNotRightCorrect) ? true : false
562
    break
563
  case 'OR':
564
    isNotLeftCorrect = recurseWhere(where.left, jsonDoc, jsonOriginalDoc)
565
    isNotRightCorrect = recurseWhere(where.right, jsonDoc, jsonOriginalDoc)
566
    isNotCorrect = (isNotLeftCorrect && isNotRightCorrect) ? true : false
567
    break
568
  case 'IN':
569
    values = getWhereValuesToCompare(where, jsonDoc, jsonOriginalDoc)
570
    isNotCorrect = isInStatementCorrect(values, true)
571
    break
572
  case 'NOT IN':
573
    values = getWhereValuesToCompare(where, jsonDoc, jsonOriginalDoc)
574
    isNotCorrect = isInStatementCorrect(values, false)
575
    break
576
  }
577
578
  return isNotCorrect
579
}